﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DomainDriver.DomainModeling.Configuration;
using DomainDriver.DomainModeling.Constraints;
using DomainDriver.DomainModeling.DataProviders;
using DomainDriver.DomainModeling.DomainModels.ConstructionResources;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Events;
using DomainDriver.DomainModeling.Exceptions;
using DomainDriver.DomainModeling.Repositories;
using DomainDriver.DomainModeling.StorageManagers;
using DomainDriver.DomainModeling.StorageManagers.DataSetStorage;
using DomainDriver.DomainModeling.StorageManagers.EntityFrameworkStorage;
using DomainDriver.DomainModeling.TransactionScripts;

namespace DomainDriver.DomainModeling.DomainModels
{
    public class DomainModel : IDomainModel, IBatchPersistableDomainModel
    {
        // Pre-Initialize Members
        private Assembly m_CallingAssembly;
        private bool m_Initialized;
        private DataSourcePersistenceType m_PersistenceType;
        private InvalidCallAction m_InvalidCallAction;
        private bool m_BuildModelWithConstraints;
        private Dictionary<string, IAssemblyRegistrar> m_SubAssemblyRegistrars;
        private Dictionary<DataSourcePersistenceType, DataSourceConstructionSpecification> m_DataSourceTypesForPersistenceTypes;
        private List<RepositoryConstructionSpecification> m_RepositoriesToConstruct;

        // Post-Initialize Members
        private IDataProvider m_DataProvider;
        private Dictionary<string, object> m_Repositories;
        private Dictionary<string, IConstraint> m_Constraints;

        // Events
        public event DataSourceConfigurationDelegate ConfigureDataSource;
        public event ConstraintCreationDelegate CreateConstraints;

        #region Constructors

        public DomainModel(DataSourcePersistenceType persistenceType, bool buildModelWithConstraints)
            : this(persistenceType, InvalidCallAction.Ignore, buildModelWithConstraints)
        { }

        public DomainModel(DataSourcePersistenceType persistenceType, InvalidCallAction invalidCallAction, bool buildModelWithConstraints)
        {
            m_CallingAssembly = Assembly.GetCallingAssembly();
            m_Initialized = false;
            m_PersistenceType = persistenceType;
            m_InvalidCallAction = invalidCallAction;
            m_BuildModelWithConstraints = buildModelWithConstraints;
            m_SubAssemblyRegistrars = new Dictionary<string, IAssemblyRegistrar>();
            m_DataSourceTypesForPersistenceTypes = new Dictionary<DataSourcePersistenceType, DataSourceConstructionSpecification>();
            m_RepositoriesToConstruct = new List<RepositoryConstructionSpecification>();
            m_Constraints = new Dictionary<string, IConstraint>();

            RegisterSubAssemblies(m_SubAssemblyRegistrars);
            RegisterDataSource<object>(DataSourcePersistenceType.None, RepositoryConstructionMethods.CreateTransientBackedRepository);

            // Register sub-assemblies
            foreach (IAssemblyRegistrar assemblyRegistrar in m_SubAssemblyRegistrars.Values)
            {
                assemblyRegistrar.RegisterDomainObjects(this);
                ConfigureDataSource += assemblyRegistrar.ConfigureDataSource;
                CreateConstraints += assemblyRegistrar.CreateConstraints;
            }
        }

        #endregion

        #region RegisterSubAssemblies

        private void RegisterSubAssemblies(Dictionary<string, IAssemblyRegistrar> subAssemblyRegistrars)
        {
            Assembly thisAssembly = Assembly.GetExecutingAssembly();

            // NOTE: First, register assemblies loaded within AppDomain
            Assembly[] activeAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly activeAssembly in activeAssemblies)
            {
                if (activeAssembly == thisAssembly)
                { continue; }

                RegisterLoadedAssembly(subAssemblyRegistrars, activeAssembly);
            }

            // NOTE: Second, register assemblies specified in App.config
            // LATER: Add App.config schema specific to DomainDriver
            string[] configuredAssemblyNames = ConfigurationManager.AppSettings.AllKeys;
            foreach (string configuredAssemblyName in configuredAssemblyNames)
            {
                if (configuredAssemblyName == thisAssembly.FullName)
                { continue; }

                Assembly configuredAssembly = Assembly.Load(configuredAssemblyName);
                RegisterLoadedAssembly(subAssemblyRegistrars, configuredAssembly);
            }
        }

        private static void RegisterLoadedAssembly(Dictionary<string, IAssemblyRegistrar> subAssemblyRegistrars, Assembly activeAssembly)
        {
            if (activeAssembly.IsDynamic)
            { return; }

            Type subAssemblyRegistrarInterfaceType = typeof(IAssemblyRegistrar);
            Type[] exportedTypes = activeAssembly.GetExportedTypes();
            foreach (Type exportedType in exportedTypes)
            {
                Type[] interfaceTypesImplemented = exportedType.GetInterfaces();

                if (interfaceTypesImplemented.Contains<Type>(subAssemblyRegistrarInterfaceType))
                {
                    ConstructorInfo constructor = exportedType.GetConstructor(new Type[] { });
                    object instance = constructor.Invoke(new object[] { });
                    IAssemblyRegistrar subAssemblyRegistrar = instance as IAssemblyRegistrar;
                    subAssemblyRegistrars.Add(activeAssembly.FullName, subAssemblyRegistrar);
                }
            }
        }

        #endregion

        #region RegisterDataSource

        public void RegisterDataSource<DATASOURCE>(DataSourcePersistenceType persistenceType, Func<Type, Type, Type, IDataProvider, Expression, IDomainModel, object> repositoryConstructionMethod)
            where DATASOURCE : new()
        {
            RegisterDataSource(persistenceType, typeof(DATASOURCE), repositoryConstructionMethod);
        }

        public void RegisterDataSource(DataSourcePersistenceType persistenceType, Type dataSourceType, Func<Type, Type, Type, IDataProvider, Expression, IDomainModel, object> repositoryConstructionMethod)
        {
            if (m_Initialized)
            { throw new ApplicationException("Cannot Register new aspects of DomainModel after Initialize() is called"); }

            if (dataSourceType == null)
            { throw new ApplicationException(); }

            ConstructorInfo defaultConstructor = dataSourceType.GetConstructor(new Type[] { });
            if (defaultConstructor == null)
            { throw new ApplicationException(); }

            DataSourceConstructionSpecification dataSourceSpecification = new DataSourceConstructionSpecification(persistenceType, dataSourceType, repositoryConstructionMethod);

            if (!m_DataSourceTypesForPersistenceTypes.ContainsKey(persistenceType))
            {
                m_DataSourceTypesForPersistenceTypes.Add(persistenceType, dataSourceSpecification);
            }
            else
            {
                m_DataSourceTypesForPersistenceTypes[persistenceType] = dataSourceSpecification;
            }
        }

        #endregion

        #region RegisterDomainObjectRepository

        public void RegisterDomainObjectRepository<KEY, KEYED_DOMAIN_OBJECT>()
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>, new()
        {
            RegisterDomainObjectRepository<KEY, KEY, KEYED_DOMAIN_OBJECT>();
        }

        public void RegisterDomainObjectRepository<KEY, KEYED_DOMAIN_OBJECT>(Expression<Func<KEYED_DOMAIN_OBJECT>> constructionMethod)
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>
        {
            RegisterDomainObjectRepository<KEY, KEY, KEYED_DOMAIN_OBJECT>(constructionMethod);
        }

        public void RegisterDomainObjectRepository<DOTNET_KEY, DB_KEY, KEYED_DOMAIN_OBJECT>()
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<DOTNET_KEY, KEYED_DOMAIN_OBJECT>, new()
        {
            RegisterDomainObjectRepository<DOTNET_KEY, DB_KEY, KEYED_DOMAIN_OBJECT>(() => new KEYED_DOMAIN_OBJECT());
        }

        public void RegisterDomainObjectRepository<DOTNET_KEY, DB_KEY, KEYED_DOMAIN_OBJECT>(Expression<Func<KEYED_DOMAIN_OBJECT>> constructionMethod)
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<DOTNET_KEY, KEYED_DOMAIN_OBJECT>
        {
            if (constructionMethod == null)
            { throw new ApplicationException(); }

            RegisterDomainObjectRepository(typeof(DOTNET_KEY), typeof(DB_KEY), typeof(KEYED_DOMAIN_OBJECT), constructionMethod);
        }

        public void RegisterDomainObjectRepository(Type dotNetKeyType, Type databaseKeyType, Type keyedDomainObjectType, Expression constructionMethod)
        {
            if (m_Initialized)
            { throw new ApplicationException("Cannot Register new aspects of DomainModel after Initialize() is called"); }

            if ((dotNetKeyType == null) || (databaseKeyType == null) || (keyedDomainObjectType == null) || (constructionMethod == null))
            { throw new ApplicationException(); }

            RepositoryConstructionSpecification constructionSpecification = new RepositoryConstructionSpecification(dotNetKeyType, databaseKeyType, keyedDomainObjectType, constructionMethod);
            m_RepositoriesToConstruct.Add(constructionSpecification);
        }

        #endregion

        #region RegisterModelConstraint

        public void AddModelConstraint(IConstraint constraint)
        {
            m_Constraints.Add(constraint.GetType().FullName, constraint);
        }

        #endregion

        #region Initialize

        public void Initialize()
        {
            if (m_Initialized)
            { throw new ApplicationException("The DomainModel can only be initialized once."); }

            m_Initialized = true;

            m_DataProvider = null;
            m_Repositories = new Dictionary<string, object>();

            // Create DataProvider
            object dataSource = CreateDataSource(m_PersistenceType);
            Type dataSourceType = m_DataSourceTypesForPersistenceTypes[m_PersistenceType].DataSourceType;
            Func<Type, Type, Type, IDataProvider, Expression, IDomainModel, object> repositoryConstructionMethod = m_DataSourceTypesForPersistenceTypes[m_PersistenceType].RepositoryConstructionSpecification;
            m_DataProvider = new DataProvider(m_PersistenceType, dataSourceType, dataSource);

            // Create Repositories
            foreach (RepositoryConstructionSpecification repositorySpecification in m_RepositoriesToConstruct)
            {
                object repository = repositoryConstructionMethod(repositorySpecification.DotNetKeyType, repositorySpecification.DatabaseKeyType, repositorySpecification.DomainObjectType, m_DataProvider, repositorySpecification.ConstructionMethod, this);
                m_Repositories.Add(repositorySpecification.DomainObjectType.FullName, repository);
            }

            // Create Constraint
            if ((CreateConstraints != null) && (m_BuildModelWithConstraints))
            {
                CreateConstraints(this, new EventArgs());
            }

            foreach (IConstraint constraint in m_Constraints.Values)
            {
                if (constraint.RequiresSetup)
                {
                    constraint.Setup();
                }
            }
        }

        public DATASOURCE CreateDataSource<DATASOURCE>(DataSourcePersistenceType persistenceType)
        {
            return (DATASOURCE)CreateDataSource(persistenceType);
        }

        public object CreateDataSource(DataSourcePersistenceType persistenceType)
        {
            Type dataSourceType = m_DataSourceTypesForPersistenceTypes[persistenceType].DataSourceType;

            if (dataSourceType == null)
            { throw new ApplicationException(); }

            ConstructorInfo defaultConstructor = dataSourceType.GetConstructor(new Type[] { });
            if (defaultConstructor == null)
            { throw new ApplicationException(); }

            object dataSource = defaultConstructor.Invoke(new object[] { });
            if (ConfigureDataSource != null)
            {
                DataSourceConfigurationArgs args = new DataSourceConfigurationArgs(dataSource);
                ConfigureDataSource(this, args);
            }

            return dataSource;
        }

        #endregion

        #region IDomainModel Implementation

        public bool BuildModelWithConstraints
        {
            get { return m_BuildModelWithConstraints; }
        }

        public DataSourcePersistenceType PersistenceType
        {
            get { return m_DataProvider.PersistenceType; }
        }

        public IDataProvider DataProvider
        {
            get { return m_DataProvider; }
        }

        public IDictionary<DataSourcePersistenceType, Type> DataSourceTypesForPersistenceTypes
        {
            get
            {
                Dictionary<DataSourcePersistenceType, Type> types = new Dictionary<DataSourcePersistenceType, Type>();
                foreach (DataSourcePersistenceType persistenceType in m_DataSourceTypesForPersistenceTypes.Keys)
                {
                    types.Add(persistenceType, m_DataSourceTypesForPersistenceTypes[persistenceType].DataSourceType);
                }
                return types;
            }
        }

        public IList<IConstraint> Constraints
        {
            get { return new List<IConstraint>(m_Constraints.Values); }
        }

        public DATA_PROVIDER GetDataProvider<DATA_PROVIDER>()
            where DATA_PROVIDER : IDataProvider
        {
            if (m_DataProvider == null)
            { return default(DATA_PROVIDER); }

            if (!m_DataProvider.GetType().Equals(typeof(DATA_PROVIDER)))
            { throw new ApplicationException(); }

            return (DATA_PROVIDER)m_DataProvider;
        }

        public IDataProvider GetDataProvider()
        {
            return m_DataProvider;
        }

        public bool HasConstraint<CONSTRAINT>()
           where CONSTRAINT : IConstraint
        {
            string typeKey = typeof(CONSTRAINT).FullName;
            bool containsKey = m_Constraints.ContainsKey(typeKey);
            return containsKey;
        }

        public CONSTRAINT GetConstraint<CONSTRAINT>()
            where CONSTRAINT : IConstraint
        {
            string typeKey = typeof(CONSTRAINT).FullName;
            if (!m_Constraints.ContainsKey(typeKey))
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because requested IConstraint Type \"{2}\" does not exist in domain model.", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            IConstraint untypedConstraint = m_Constraints[typeKey];
            if (!untypedConstraint.GetType().Equals(typeof(CONSTRAINT)))
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because requested IConstraint Type \"{2}\" does not exist in domain model.", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            return (CONSTRAINT)untypedConstraint;
        }

        public bool HasRepository<KEY, KEYED_DOMAIN_OBJECT>()
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>
        {
            string typeKey = typeof(KEYED_DOMAIN_OBJECT).FullName;
            bool containsKey = m_Repositories.ContainsKey(typeKey);
            return containsKey;
        }

        public IRepository<KEY, KEYED_DOMAIN_OBJECT> GetRepository<KEY, KEYED_DOMAIN_OBJECT>()
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>
        {
            string typeKey = typeof(KEYED_DOMAIN_OBJECT).FullName;

            if (!m_Repositories.ContainsKey(typeKey))
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because no repository exists in the domain model for the requested IDomainObject Type \"{2}\".", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            object untypedRepository = m_Repositories[typeKey];
            IRepository<KEY, KEYED_DOMAIN_OBJECT> typedRepository = untypedRepository as IRepository<KEY, KEYED_DOMAIN_OBJECT>;

            if (typedRepository == null)
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because no repository exists in the domain model for the requested IDomainObject Type \"{2}\".", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            return typedRepository;
        }

        public TRANSACTION_SCRIPT CreateTransactionScript<TRANSACTION_SCRIPT>()
            where TRANSACTION_SCRIPT : ITransactionScript, new()
        {
            TRANSACTION_SCRIPT transactionScript = new TRANSACTION_SCRIPT();
            transactionScript.DomainModel = this;
            return transactionScript;
        }

        #endregion

        #region IBatchPersistableDomainModel Implementation

        public event ChangingDataSourceDelegate LoadingFromDataSource;
        public event ChangedDataSourceDelegate LoadedFromDataSource;
        public event ChangingDataSourceDelegate PersistingToDataSource;
        public event ChangedDataSourceDelegate PersistedToDataSource;

        public InvalidCallAction InvalidCallAction
        {
            get { return m_InvalidCallAction; }
        }

        public virtual void LoadFromDataSource<DATASOURCE>(DATASOURCE dataSource)
        {
            if (PersistenceType != DataSourcePersistenceType.File)
            {
                if (InvalidCallAction == InvalidCallAction.ThrowException)
                { throw new ApplicationException("The domain model does not support batch persistence."); }
                else
                { return; }
            }
            if (!dataSource.GetType().Equals(DataProvider.DataSourceType))
            {
                throw new ApplicationException("The supplied datasource type does not match the type currently in use.");
            }

            DataProvider.DataSource = dataSource;
            Dictionary<IConstraint, bool> constraintsEnabled = new Dictionary<IConstraint, bool>();
            foreach (IConstraint constraint in Constraints)
            {
                constraintsEnabled[constraint] = constraint.Enabled;
                constraint.Enabled = false;
            }

            if (LoadingFromDataSource != null)
            {
                LoadingFromDataSource(this, new EventArgs());
            }
            if (LoadedFromDataSource != null)
            {
                LoadedFromDataSource(this, new EventArgs());
            }

            foreach (IConstraint constraint in Constraints)
            {
                constraint.Enabled = constraintsEnabled[constraint];
            }
            DataProvider.DataSource = null;
        }

        public virtual void PersistToDataSource<DATASOURCE>(DATASOURCE dataSource)
        {
            if (PersistenceType != DataSourcePersistenceType.File)
            {
                if (InvalidCallAction == InvalidCallAction.ThrowException)
                { throw new ApplicationException("The domain model does not support batch persistence."); }
                else
                { return; }
            }
            if (!dataSource.GetType().Equals(DataProvider.DataSourceType))
            {
                throw new ApplicationException("The supplied datasource type does not match the type currently in use.");
            }

            DataProvider.DataSource = dataSource;
            DataSet dataSourceAsDataSet = (dataSource as DataSet);
            if (dataSourceAsDataSet != null)
            { dataSourceAsDataSet.EnforceConstraints = false; }

            if (PersistingToDataSource != null)
            {
                PersistingToDataSource(this, new EventArgs());
            }
            if (PersistedToDataSource != null)
            {
                PersistedToDataSource(this, new EventArgs());
            }

            if (dataSourceAsDataSet != null)
            {
                dataSourceAsDataSet.EnforceConstraints = true;
                dataSourceAsDataSet.AcceptChanges();
            }
            DataProvider.DataSource = null;
        }

        #endregion
    }
}